/*
 * TODO: License
 * 2007, JTrapp@google.com
 */
package com.google.cse.emea.universaltransformer.editors;

import com.google.cse.emea.jct.utils.protocols.cpath.Handler;
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import java.nio.charset.UnsupportedCharsetException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.logging.Logger;
import javax.swing.event.TableModelEvent;
import javax.swing.event.TableModelListener;
import javax.swing.table.TableModel;

/**
 * A table model, that is loaded and filled from a csv table file. The input file
 * has to be in text format. To prevent large tables from scrutinizing the ui, you
 * may specify a max number of lines, that should be read from file. The model can
 * also be used as LineReader, which offers a read method that returns the table 
 * row by row each time, it is called.
 * This table model is used to compare two tables and to allow merge and diff and
 * filtering. For the comparison, it is assumed, that each table has an id column
 * that can be used to map the rows to each other.
 *   
 * @author jtrapp@Google.com
 */
class CSVTableModel implements TableModel {
    public static final int MAX_WARNINGS = 6; //To prevent warning spam, the number of warnings is restricted
    public static String[] EMPTY = new String[0]; //Empty table
    private String filename = null; // The filename of the table
    private String delimiter; //What is the column separator
    private String charset; // The charset name
    private String[] header; // the header cells
    private boolean fullyLoaded = true; //The table may not be loaded fully, if maxRows restricts it
    
    /** Store the rows of the tables in an ordered way. Header line will not be stored here.*/
    private ArrayList<String[]> rows = new ArrayList<String[]>();
    private int maxWarnings = 0; // maximum numbers of warnings. (to prevent warning spamming
    
    /** Listener for change events.*/
    private List<TableModelListener> listeners = new ArrayList<TableModelListener>();


/** 
 * Initialize the model with all necessary values for loading the data 
 * from file.
 * 
 * @param filename The CSV Table.
 * @param maxRows Maximum number of rows, that shall be read
 * @param delimiter The character(s), that separate the cells in a row
 * @param encoding The charset of the file. Use UTF-8 if in doubt, Excel uses UTF-16LE
 * @param idColumn Na
 * @throws java.io.FileNotFoundException
 * @throws java.io.IOException
 */
   
 public CSVTableModel(String filename, int maxRows, String delimiter, String encoding) throws FileNotFoundException, IOException {
        this.filename = filename;
        this.delimiter = delimiter;
        this.delimiter = getDelimiterForName(delimiter);
        this.charset = encoding;
        rows = new ArrayList<String[]>();
        load(maxRows);
    }

    /** Empty Model. Nothing to show*/
    CSVTableModel() {
    }



    /**
     * Append a row to the model 
     * @param row cell values of the row.
     */
    public void add(String[] row) {
        rows.add(row);

        fireUpdatedRow(rows.size() - 1);
    }

     /**
     * Append a row to the model 
     * @param row cell values of the row.
     */
    public void add(String line) {
        add(line.split(delimiter));
    }

    /**
     * Number of rows (without header line) 
     * @return Number of rows
     */
    public int getRowCount() {
        if(rows == null) {
            return 0;
        } else {
            return rows.size();
        }
    }

    /** Number of columns, determined by the cells in the header line. Other lines
     * in the row may contain more items.
     * TODO: change the way columnNumber is handled. shoud use max of all rows.
     * @return Number of Columns in the header.
     */ 
    public int getColumnCount() {
        if (header == null) {
            return 0;
        } else {
            return header.length;
        }
    }

    /**
     * Get cell value.
     * @param rowIndex row
     * @param columnIndex column
     * @return cell value (or "" if no cell is available for that row or column)
     */
    public Object getValueAt(int rowIndex, int columnIndex) {
        String[] row = rows.get(rowIndex);
        if ((row!=null)&&(row.length > columnIndex)) {
            return row[columnIndex];
        } else {
            return "";
        }
    }

    /** The row values (without the header line. 
     * 
     * @return the rows.
     */
    public ArrayList<String[]> getRows() {
        return rows;
    }

    /**
     * Get the cells of the row
     * @param row Row Index
     * @return Cell values.
     */
    String[] getCells(int row) {
        return rows.get(row);
    }

    /**
     * To prevent the model from eating all memory, the maximum number of lines,
     * that are read from file can be restricted.
     * @return true, if all files have been loaded; false if not all files have been loaded
     */
    boolean isFullyLoaded() {
        return fullyLoaded;
    }

    /** Since the delimiter may contain special characters, names can be used.
     * TODO: use properties file
     * @param delimiter Character(s) that are used to split lines, Constant names may be used
     * @return the charater used to separate 
     */
    public static String getDelimiterForName(String delimiter) {
        if ((delimiter==null) || delimiter.equalsIgnoreCase("TAB")) {
            return "\t";
        } else if (delimiter.equalsIgnoreCase("SPACE")) {
             return " ";
        } else if (delimiter.equalsIgnoreCase("SPACES")) {
            return "[ ]*";
        } else {
            return delimiter;
        }
            
    }
 
        /**
         * Return a human readable name for the pattern, that is used as a delimiter
        * TODO: use properties file
         * @param delimiter Delimiter Pattern
         * @return The name of the pattern or (if not standard) the delimiter pattern itself.
         */
        public static String getNameForDelimiter(String delimiter) {

               if (delimiter.equals("\t")) {
            return "TAB";
        } else if (delimiter.equals(" ")) {
            return "SPACE";
        } else if (delimiter.equals("[ ]*")) {
            return "SPACES";
        }
            return delimiter;
        }
    /**
     * Inform all listeners, that something has changed. 
     */
    private void fireUpdatedTable() {
        for (TableModelListener l : listeners) {
            l.tableChanged(new TableModelEvent(this));
        }
    }

    /**
     * Inform all listeners, that something has changed. 
     * @param row Changed row
     */
    private void fireUpdatedRow(int row) {
        for (TableModelListener l : listeners) {
            l.tableChanged(new TableModelEvent(this, row));
        }
    }

    /**
     * Inform all listeners, that something has changed. 
     * @param row Changed row
     * @param column Changed column
     */
    private void fireUpdatedCell(int row, int column) {
        for (TableModelListener l : listeners) {
            l.tableChanged(new TableModelEvent(this, row, row, column));
        }
    }

    /** Get the column index for a given column name.
     * 
     * @param idColumnName column name of the id column
     * @return the index of the column name 
     */
    private int getColumnIndex(String idColumnName) {
        for (int i = 0; i < header.length; i++) {
            if (header[i].trim().equals(idColumnName)) {
                return i;
            }
        }
        return -1;
    }

    /** Load the model from file.
     * 
     * @param maxRows Max number of rows that should be loaded
     * @return the number of lines loaded
     * @throws java.io.FileNotFoundException File not found
     * @throws java.io.IOException Some IOError
     */
    private int load(int maxRows) throws FileNotFoundException, IOException {
        rows = new ArrayList<String[]>();
        header = EMPTY;
        if(filename==null){
            return 0;
        }
        InputStream stream = Handler.getStream(filename);
        Charset incharset = getCharset(getCharset());
        BufferedReader reader = new BufferedReader(new InputStreamReader(stream, incharset));

        int lines = 0;
        String line = null;
        do { // read first non empty line as header.
            line = reader.readLine();
        } while ((line != null) && (line.length() == 0));

        if (line != null) {
            header = line.split(delimiter); // Store Header


            while ((lines < maxRows) && ((line = reader.readLine()) != null)) {
                String[] row = line.split(delimiter);
                add(row);
                
                lines++;
            }
            fullyLoaded = (line == null);
        }
        reader.close();
        fireUpdatedTable();
        return lines;
    }

    public String getColumnName(int columnIndex) {
        return header[columnIndex];
    }

    /**
     * Is the cell editable. 
     * @param rowIndex The row Index
     * @param columnIndex The column index
     */
    public boolean isCellEditable(int rowIndex, int columnIndex) {
        return true;
    }

    /**
     * Set Value in cell
     * @param aValue The value
     * @param rowIndex The row Index
     * @param columnIndex The column index
     */
    public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
        String[] row = rows.get(rowIndex);
        if (columnIndex<row.length) {
            row[columnIndex] = aValue.toString();
        }
        else if (row.length<getColumnCount()) { 
            // Since during loading the table, the number of columns is not fixed
            String newRow[] = new String[getColumnCount()];
            System.arraycopy(row, 0, newRow, 0, row.length);
            newRow[columnIndex] = aValue.toString();
            rows.remove(rowIndex);
            rows.add(rowIndex, newRow);
        } else {
            throw new ArrayIndexOutOfBoundsException("Column index out of bounds for row "+rowIndex);
        }
        fireUpdatedCell(rowIndex, columnIndex);
    }
    
    


    /**
     * Add a alistener for changes
     * @param l The Listener
     */
    public void addTableModelListener(TableModelListener l) {
        listeners.add(l);
    }

    /**
     * REmove a alistener for changes
     * @param l The Listener
     */    public void removeTableModelListener(TableModelListener l) {
        listeners.remove(l);
    }
    // This method returns the Class object of the first
        // cell in specified column in the table model.
        // Unless this method is overridden, all values are
        // assumed to be the type Object.

    public Class<?> getColumnClass(int columnIndex) {
        return String.class;
    }



    /** 
     * Get the table header.
     * 
     * @return Header Cells
     */
    public String[] getHeader() {
        return header;
    }

    /**
     * Get the filename that has bee used to load the file.
     * @return the filename used in the model
     */public String getFilename() {
        return filename;
    }

    
    int cursor = 0;

    public String[] open() throws IOException {
        cursor = 0;
        return header;
    }

    /**
     * Close is required for the Line Reader interface, but it is nothing to do
     * in htis implementation.
     * @throws java.io.IOException Can never be thrown, because nothing is done.
     */
    public void close() throws IOException {
    //nothing to do here
    }


/** 
 * Get the next line from the table.
 * @return The cells of the row
 * @throws java.io.IOException Will never be thrown, because no IO operation is necessary.
 */
    public String[] readLine() throws IOException {
        if(cursor<rows.size()){
            return rows.get(cursor++);
        } else {
            return null;
        }
    }

    /**
     * Return the delimiter used to split the line
     * @return The delimiter
     */public String getDelimiter() {
        return delimiter;
    }

     /** 
      * The charset that has been used to read the rows from file.
      * 
      * @return The Charset name
      */
    public String getCharset() {
        return charset;
    }

    
    /** Get a charset for name.
     */
    public static Charset getCharset(String name) {
        try {
            return Charset.forName(name);
        } catch (UnsupportedCharsetException e) {

            return Charset.forName("UTF-8");
        }
    }
    
    
}
